1 /* 2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 3 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 4 Authors: Marcelo S. N. Mancini 5 6 Copyright Marcelo S. N. Mancini 2018 - 2021. 7 Distributed under the CC BY-4.0 License. 8 (See accompanying file LICENSE.txt or copy at 9 https://creativecommons.org/licenses/by/4.0/ 10 */ 11 module hip.graphics.g2d.spritebatch; 12 import hip.graphics.mesh; 13 import hip.graphics.orthocamera; 14 import hip.hiprenderer.renderer; 15 import hip.assets.texture; 16 import hip.hiprenderer.framebuffer; 17 import hip.error.handler; 18 import hip.hiprenderer.shader; 19 public import hip.api.graphics.batch; 20 public import hip.api.graphics.color; 21 public import hip.math.vector; 22 public import hip.math.matrix; 23 24 /** 25 * This is what to expect in each vertex sent to the sprite batch 26 */ 27 @HipShaderInputLayout struct HipSpriteVertex 28 { 29 Vector3 vPosition = Vector3.zero; 30 HipColor vColor = HipColor.white; 31 Vector2 vTexST = Vector2.zero; 32 float vTexID = 0; 33 34 static enum floatCount = cast(size_t)(HipSpriteVertex.sizeof/float.sizeof); 35 static enum quadCount = floatCount*4; 36 // static assert(HipSpriteVertex.floatCount == 10, "SpriteVertex should contain 9 floats and 1 int"); 37 } 38 39 @HipShaderVertexUniform("Cbuf1") 40 struct HipSpriteVertexUniform 41 { 42 Matrix4 uModel = Matrix4.identity; 43 Matrix4 uView = Matrix4.identity; 44 Matrix4 uProj = Matrix4.identity; 45 } 46 47 @HipShaderFragmentUniform("Cbuf") 48 struct HipSpriteFragmentUniform 49 { 50 float[4] uBatchColor = [1,1,1,1]; 51 52 @(ShaderHint.Blackbox | ShaderHint.MaxTextures) 53 IHipTexture[] uTex; 54 } 55 56 /** 57 * The spritebatch contains 2 shaders. 58 * One shader is entirely internal, which you don't have any control, this is for actually being able 59 * to draw stuff on the screen. 60 * 61 * The another one is a post processing shader, which the spritebatch doesn't uses by default. If 62 * setPostProcessingShader() 63 */ 64 class HipSpriteBatch : IHipBatch 65 { 66 index_t maxQuads; 67 index_t[] indices; 68 HipSpriteVertex[] vertices; 69 70 protected bool hasInitTextureSlots; 71 protected Shader spriteBatchShader; 72 73 ///Post Processing Shader 74 protected Shader ppShader; 75 protected HipFrameBuffer fb; 76 protected HipTextureRegion fbTexRegion; 77 protected float managedDepth = 0; 78 79 HipOrthoCamera camera; 80 Mesh mesh; 81 82 protected IHipTexture[] currentTextures; 83 int usingTexturesCount; 84 85 uint lastDrawQuadsCount = 0; 86 uint quadsCount; 87 88 89 this(HipOrthoCamera camera = null, index_t maxQuads = 10_900) 90 { 91 import hip.util.conv:to; 92 ErrorHandler.assertLazyExit(index_t.max > maxQuads * 6, "Invalid max quads. Max is "~to!string(index_t.max/6)); 93 this.maxQuads = maxQuads; 94 indices = new index_t[maxQuads*6]; 95 vertices = new HipSpriteVertex[maxQuads]; //XYZ -> 3, RGBA -> 4, ST -> 2, TexID 3+4+2+1=10 96 vertices[] = HipSpriteVertex.init; 97 currentTextures = new IHipTexture[](HipRenderer.getMaxSupportedShaderTextures()); 98 usingTexturesCount = 0; 99 100 this.spriteBatchShader = HipRenderer.newShader(HipShaderPresets.SPRITE_BATCH); 101 spriteBatchShader.addVarLayout(ShaderVariablesLayout.from!HipSpriteVertexUniform); 102 spriteBatchShader.addVarLayout(ShaderVariablesLayout.from!HipSpriteFragmentUniform); 103 spriteBatchShader.setBlending(HipBlendFunction.SRC_ALPHA, HipBlendFunction.ONE_MINUS_SRC_ALPHA, HipBlendEquation.ADD); 104 105 mesh = new Mesh(HipVertexArrayObject.getVAO!HipSpriteVertex, spriteBatchShader); 106 mesh.vao.bind(); 107 mesh.createVertexBuffer(cast(index_t)(maxQuads*HipSpriteVertex.quadCount), HipBufferUsage.DYNAMIC); 108 mesh.createIndexBuffer(cast(index_t)(maxQuads*6), HipBufferUsage.STATIC); 109 110 111 112 spriteBatchShader.useLayout.Cbuf; 113 // spriteBatchShader.bind(); 114 // spriteBatchShader.sendVars(); 115 116 mesh.sendAttributes(); 117 118 119 spriteBatchShader.useLayout.Cbuf; 120 spriteBatchShader.bind(); 121 spriteBatchShader.sendVars(); 122 123 if(camera is null) 124 camera = new HipOrthoCamera(); 125 this.camera = camera; 126 HipVertexArrayObject.putQuadBatchIndices(indices, maxQuads); 127 mesh.setVertices(vertices); 128 mesh.setIndices(indices); 129 setTexture(HipTexture.getPixelTexture()); 130 } 131 void setCurrentDepth(float depth){managedDepth = depth;} 132 133 void setShader(Shader s) 134 { 135 if(fb is null) 136 { 137 Viewport v = HipRenderer.getCurrentViewport; 138 fb = HipRenderer.newFrameBuffer(cast(int)v.width, cast(int)v.height); 139 // fbTexRegion = new HipTextureRegion(fb.getTexture()); 140 } 141 this.ppShader = s; 142 } 143 144 /** 145 * Sets the texture slot/index for the current quad and points it to the next quad 146 */ 147 void addQuad(void[] quad, int slot) 148 { 149 if(quadsCount+1 > maxQuads) 150 flush(); 151 152 size_t start = quadsCount; 153 version(none) //D way to do it, but it is also slower 154 { 155 size_t end = start + HipSpriteVertex.quadCount; 156 vertices[start..end] = quad; 157 vertices[start+ T1] = slot; 158 vertices[start+ T2] = slot; 159 vertices[start+ T3] = slot; 160 vertices[start+ T4] = slot; 161 } 162 else 163 { 164 import core.stdc.string; 165 HipSpriteVertex* v = cast(HipSpriteVertex*)vertices.ptr; 166 memcpy(v + start, quad.ptr, HipSpriteVertex.sizeof * 4); 167 v[0].vTexID = slot; 168 v[1].vTexID = slot; 169 v[2].vTexID = slot; 170 v[3].vTexID = slot; 171 } 172 173 quadsCount++; 174 } 175 176 void addQuads(void[] quadsVertices, int slot) 177 { 178 import hip.util.array:swapAt; 179 assert(quadsVertices.length % (HipSpriteVertex.sizeof*4) == 0, "Count must be divisible by HipSpriteVertex.sizeof*4"); 180 HipSpriteVertex[] v = cast(HipSpriteVertex[])quadsVertices; 181 uint countOfQuads = cast(uint)(v.length / 4); 182 183 184 while(countOfQuads > 0) 185 { 186 size_t remainingQuads = this.maxQuads - this.quadsCount; 187 if(remainingQuads == 0) 188 { 189 flush(); 190 this.usingTexturesCount = 1; 191 swapAt(this.currentTextures, 0, slot);//Guarantee the target slot is being used 192 remainingQuads = this.maxQuads; 193 } 194 size_t quadsToDraw = (countOfQuads < remainingQuads) ? countOfQuads : remainingQuads; 195 196 size_t start = quadsCount; 197 size_t end = (start + quadsToDraw)*4; 198 199 vertices[start..end] = v; 200 for(int i = 0; i < quadsToDraw; i++) 201 { 202 vertices[start + i].vTexID = slot; 203 vertices[start + i+1].vTexID = slot; 204 vertices[start + i+2].vTexID = slot; 205 vertices[start + i+3].vTexID = slot; 206 } 207 v = v[quadsToDraw..$]; 208 209 if(quadsToDraw + remainingQuads == maxQuads) 210 { 211 flush(); 212 this.usingTexturesCount = 1; 213 swapAt(this.currentTextures, 0, slot);//Guarantee the target slot is being used 214 } 215 else 216 this.quadsCount+= quadsToDraw; 217 countOfQuads-= quadsToDraw; 218 } 219 } 220 221 private int getNextTextureID(IHipTexture t) 222 { 223 for(int i = 0; i < usingTexturesCount; i++) 224 if(currentTextures[i] is t) 225 return i; 226 if(usingTexturesCount < currentTextures.length) 227 { 228 currentTextures[usingTexturesCount] = t; 229 return usingTexturesCount++; 230 } 231 return -1; 232 } 233 /** 234 * Sets the current texture in use on the sprite batch and returns its slot. 235 */ 236 protected int setTexture (IHipTexture texture) 237 { 238 int slot = getNextTextureID(texture); 239 if(slot == -1) 240 { 241 flush(); 242 slot = getNextTextureID(texture); 243 } 244 return slot; 245 } 246 protected int setTexture(IHipTextureRegion reg){return setTexture(reg.getTexture());} 247 248 protected static bool isZeroAlpha(void[] vertices) 249 { 250 HipSpriteVertex[] v = cast(HipSpriteVertex[])vertices; 251 return v[0].vColor.a == 0 && v[1].vColor.a == 0 && v[2].vColor.a == 0 && v[3].vColor.a == 0; 252 } 253 254 void draw(IHipTexture t, ubyte[] vertices) 255 { 256 if(isZeroAlpha(vertices)) return; 257 ErrorHandler.assertExit(t.getWidth != 0 && t.getHeight != 0, "Tried to draw 0 bounds sprite"); 258 int slot = setTexture(t); 259 ErrorHandler.assertExit(slot != -1, "HipTexture slot can't be -1 on draw phase"); 260 261 if(vertices.length == HipSpriteVertex.quadCount) 262 addQuad(vertices, slot); 263 else 264 addQuads(vertices, slot); 265 } 266 267 void draw(IHipTexture texture, int x, int y, int z = 0, in HipColor color = HipColor.white, float scaleX = 1, float scaleY = 1, float rotation = 0) 268 { 269 import hip.global.gamedef; 270 if(color.a == 0) return; 271 if(quadsCount+1 > maxQuads) 272 flush(); 273 if(texture is null) 274 texture = cast()getDefaultTexture(); 275 ErrorHandler.assertExit(texture.getWidth() != 0 && texture.getHeight() != 0, "Tried to draw 0 bounds texture"); 276 int slot = setTexture(texture); 277 ErrorHandler.assertExit(slot != -1, "HipTexture slot can't be -1 on draw phase"); 278 279 size_t startVertex = quadsCount *4; 280 size_t endVertex = startVertex + 4; 281 282 getTextureVertices(vertices[startVertex..endVertex], slot, texture,x,y,managedDepth,color, scaleX, scaleY, rotation); 283 quadsCount++; 284 } 285 286 287 void draw(IHipTextureRegion reg, int x, int y, int z = 0, in HipColor color = HipColor.white, float scaleX = 1, float scaleY = 1, float rotation = 0) 288 { 289 if(color.a == 0) return; 290 if(quadsCount+1 > maxQuads) 291 flush(); 292 ErrorHandler.assertExit(reg.getWidth() != 0 && reg.getHeight() != 0, "Tried to draw 0 bounds region"); 293 int slot = setTexture(reg); 294 ErrorHandler.assertExit(slot != -1, "HipTexture slot can't be -1 on draw phase"); 295 size_t startVertex = quadsCount*4; 296 size_t endVertex = startVertex + 4; 297 298 getTextureRegionVertices(vertices[startVertex..endVertex], slot, reg,x,y,managedDepth,color, scaleX, scaleY, rotation); 299 quadsCount++; 300 } 301 302 private static void setColor(HipSpriteVertex[] ret, in HipColor color) 303 { 304 ret[0].vColor = color; 305 ret[1].vColor = color; 306 ret[2].vColor = color; 307 ret[3].vColor = color; 308 } 309 310 private static void setZ(HipSpriteVertex[] vertices, float z) 311 { 312 vertices[0].vPosition.z = z; 313 vertices[1].vPosition.z = z; 314 vertices[2].vPosition.z = z; 315 vertices[3].vPosition.z = z; 316 } 317 private static void setUV(HipSpriteVertex[] vertices, const scope ref float[8] uv) 318 { 319 vertices[0].vTexST = Vector2(uv[0], uv[1]); 320 vertices[1].vTexST = Vector2(uv[2], uv[3]); 321 vertices[2].vTexST = Vector2(uv[4], uv[5]); 322 vertices[3].vTexST = Vector2(uv[6], uv[7]); 323 } 324 private static void setTID(HipSpriteVertex[] vertices, int tid) 325 { 326 vertices[0].vTexID = tid; 327 vertices[1].vTexID = tid; 328 vertices[2].vTexID = tid; 329 vertices[3].vTexID = tid; 330 } 331 private static void setBounds(HipSpriteVertex[] vertices, float x, float y, float width, float height, float scaleX = 1, float scaleY = 1) 332 { 333 width*= scaleX; 334 height*= scaleY; 335 vertices[0].vPosition.xy = Vector2(x, y); 336 vertices[1].vPosition.xy = Vector2(x+width, y); 337 vertices[2].vPosition.xy = Vector2(x+width, y+height); 338 vertices[3].vPosition.xy = Vector2(x, y+height); 339 } 340 341 private static void setBoundsFromRotation(HipSpriteVertex[] vertices, float x, float y, float width, float height, float rotation, float scaleX = 1, float scaleY = 1) 342 { 343 import hip.math.utils:cos,sin; 344 width*= scaleX; 345 height*= scaleY; 346 float centerX = -width/2; 347 float centerY = -height/2; 348 float x2 = x + width; 349 float y2 = y + height; 350 float c = cos(rotation); 351 float s = sin(rotation); 352 353 vertices[0].vPosition.xy = Vector2(c*centerX - s*centerY + x, c*centerY + s*centerX + y); 354 vertices[1].vPosition.xy = Vector2(c*x2 - s*centerY + x, c*centerY + s*x2 + y); 355 vertices[2].vPosition.xy = Vector2(c*x2 - s*y2 + x, c*y2 + s*x2 + y); 356 vertices[3].vPosition.xy = Vector2(c*centerX - s*y2 + x, c*y2 + s*centerX + y); 357 } 358 359 360 static void getTextureVertices(HipSpriteVertex[] output, int slot, IHipTexture texture, 361 int x, int y, float z = 0, in HipColor color = HipColor.white, float scaleX = 1, float scaleY = 1, float rotation = 0) 362 { 363 int width = texture.getWidth(); 364 int height = texture.getHeight(); 365 366 const float[8] v = HipTextureRegion.defaultVertices; 367 setUV(output, v); 368 setZ(output, z); 369 setTID(output, slot); 370 setColor(output, color); 371 if(rotation == 0) 372 setBounds(output, x, y, width, height, scaleX, scaleY); 373 else 374 setBoundsFromRotation(output, x, y, width, height, rotation, scaleX, scaleY); 375 } 376 377 static void getTextureRegionVertices(HipSpriteVertex[] output, int slot, IHipTextureRegion reg, 378 int x, int y, float z = 0, in HipColor color = HipColor.white, float scaleX = 1, float scaleY = 1, float rotation = 0) 379 { 380 int width = reg.getWidth(); 381 int height = reg.getHeight(); 382 setZ(output, z); 383 setColor(output, color); 384 setTID(output, slot); 385 setUV(output, reg.getVertices()); 386 if(rotation == 0) 387 setBounds(output, x, y, width, height, scaleX, scaleY); 388 else 389 setBoundsFromRotation(output, x, y, width, height, rotation, scaleX, scaleY); 390 } 391 392 393 394 void draw() 395 { 396 if(quadsCount - lastDrawQuadsCount != 0) 397 { 398 for(int i = usingTexturesCount; i < currentTextures.length; i++) 399 currentTextures[i] = currentTextures[0]; 400 mesh.bind(); 401 402 mesh.shader.setVertexVar("Cbuf1.uProj", camera.proj, false); 403 mesh.shader.setVertexVar("Cbuf1.uModel",Matrix4.identity(), false); 404 mesh.shader.setVertexVar("Cbuf1.uView", camera.view, false); 405 mesh.shader.setFragmentVar("Cbuf.uTex", currentTextures); 406 mesh.shader.bindArrayOfTextures(currentTextures, "uTex"); 407 mesh.shader.sendVars(); 408 409 size_t start = lastDrawQuadsCount*4; 410 size_t end = quadsCount*4; 411 mesh.updateVertices(cast(void[])vertices[start..end],cast(int)start); 412 mesh.draw((quadsCount-lastDrawQuadsCount)*6, HipRendererMode.TRIANGLES, lastDrawQuadsCount*6); 413 414 ///Some operations may require texture unbinding(D3D11 Framebuffer) 415 foreach(i; 0..usingTexturesCount) 416 currentTextures[i].unbind(i); 417 mesh.unbind(); 418 } 419 lastDrawQuadsCount = quadsCount; 420 } 421 422 void flush() 423 { 424 if(ppShader !is null) 425 fb.bind(); 426 draw(); 427 lastDrawQuadsCount = quadsCount = usingTexturesCount = 0; 428 if(ppShader !is null) 429 { 430 fb.unbind(); 431 draw(fbTexRegion, 0,0 ); 432 draw(); 433 } 434 lastDrawQuadsCount = quadsCount = usingTexturesCount = 0; 435 } 436 }